﻿#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

//当我们发现程序中存在的问题的时候，那下⼀步就是找到问题，并修复问题。
//这个找问题的过程叫称为调试，英⽂叫debug（消灭bug）的意思。
//调试⼀个程序，⾸先是承认出现了问题，然后通过各种⼿段去定位问题的位置，可能是逐过程的调
//试，也可能是隔离和屏蔽代码的⽅式，找到问题所的位置，然后确定错误产⽣的原因，再修复代码，
//重新测试。
// 调试其实就是当程序已经存在问题的时候，去找问题的过程，调试是一个动作

//Debug 通常称为调试版本，它包含调试信息，并且不作任何优化，便于程序员调试程序；
//程序员在写代码的时候，需要经常性的调试代码，就将这⾥设置为 debug ，这样编译产⽣的是
//debug 版本的可执⾏程序，其中包含调试信息，是可以直接调试的。
//Release 称为发布版本，它往往是进⾏了各种优化，使得程序在代码⼤⼩和运⾏速度上都是最优的，
//以便⽤⼾很好地使⽤。当程序员写完代码，测试再对程序进⾏测试，直到程序的质量符合交付给⽤⼾
//使⽤的标准，这个时候就会设置为 release ，编译产⽣的就是 release 版本的可执⾏程序，这个
//版本是⽤⼾使⽤的，⽆需包含调试信息等。

// 测试人员测的是release版本的
//编译⽣成的可执⾏⽂件的⼤⼩，release版本明显要⼩，⽽debug版本明
//显⼤。

//int main()
//{
//	int arr[10] = { 0 };
//	int i = 0;
//
//	// 给数组元素赋值
//	for (i = 0; i < 10; i++)
//	{
//		arr[i] = i + 1;  //这里循环要按10次f5，才能到下一个断点
//		// 如果我们已经知道前五次循环没有问题 我们只想在第六次循环断点，我们应该怎么做呢
//		// 鼠标右击红色的点 设置条件i==5 这样就会在第六次循环才会触发断点 条件断点 然后就直接跳到下一次断点处了
//		// 只有i==5一个断点
//		//scanf("%d ", &arr[i]);  // 按f5调试的时候会先到这里输入值，才到下一个断点
//	}
//
//	//打印数组的内容
//	for (i = 0; i < 10; i++)
//	{
//		printf("%d ", arr[i]);
//	}
//	return 0;
//}

//调试最常使⽤的⼏个快捷键：
//F9：创建断点和取消断点
//断点的作⽤是可以在程序的任意位置设置断点，打上断点就可以使得程序执⾏到想要的位置暂定执
//⾏，接下来我们就可以使⽤F10，F11这些快捷键，观察代码的执⾏细节。
//条件断点：满⾜这个条件，才触发断点
//F5：启动调试，经常⽤来直接跳到下⼀个断点处，⼀般是 和F9配合使⽤。运行逻辑上的下一个断点 而不是物理上的下一个断点
//F10：逐过程，通常⽤来处理⼀个过程，⼀个过程可以是⼀次函数调⽤，或者是⼀条语句。
//F11：逐语句，就是每次都执⾏⼀条语句，但是这个快捷键可以使我们的执⾏逻辑进⼊函数内部。在函
//数调⽤的地⽅，想进⼊函数观察细节，必须使⽤F11，如果使⽤F10，直接完成函数调⽤。
//CTRL + F5：开始执⾏不调试，如果你想让程序直接运⾏起来⽽不调试就可以直接使⽤。 有断点也不会去管

// 我们想一个一个过程就按f10 进不到函数内部  f11是一条条语句来 就会进入函数内部

//void test()
//{
//	printf("test\n");
//	int n = 4 + 5;
//	printf("%d\n", n);
//}
//int main()
//{
//	int a = 10;
//	int b = 20;
//	int c = a + b;
//	test();
//	printf("hehe\n");
//	return 0;
//}

//在调试的过程中我们，如果要观察代码执⾏过程中，上下⽂环境中的变量的值，有哪些⽅法呢？
//这些观察的前提条件⼀定是开始调试后观察，
//开始调试后，在菜单栏中【调试】->【窗⼝】->【监视】，打开任意⼀个监视窗⼝，输⼊想要观察的对
//象就⾏。
//如果监视窗⼝看的不够仔细，也是可以观察变量在内存中的存储情况，还是在【调试】->【窗⼝】->
//【内存】

//在打开内存窗⼝后，要在地址栏输⼊：arr，& num，& c，这类地址，就能观察到该地址处的数据。
//int main()
//{
//	int arr[10] = { 0 };
//	int num = 100;
//	char c = 'w';
//
//	int i = 0;
//	for (i = 0; i < 10; i++)
//	{
//		arr[i] = i;
//	}
//	return 0;
//}

//1字节 = 8位二进制（bit)  一个16进制位对应4个2进制位
// 内存中是以16进制位展示的 所有两个16进制位就1个字节

//求 1!+ 2!+ 3!+ 4!+ ...10!的和，请看下⾯的代码：
//int main()
//{
//	int n = 0;
//	scanf("%d", &n);
//	int i = 0;
//	int ret = 1;
//	for (i = 1;i <= n; i++)
//	{
//		ret = ret * i;  // 求阶乘
//	}
//	printf("%d\n", ret);
//	return 0;
//}

//int main()
//{
//	int n = 0;
//	int i = 0;
//	int ret = 1;  // 不能从0开始 因为是乘
//	int sum = 0;
//	for (n = 1; n <= 3; n++)
//	{
//		ret = 1; // 调试发现的错误，再加上这个
//		for (i = 1; i <= n; i++)
//		{
//			ret *=i;  // 求阶乘 ret = ret * i
//		}
//		sum += ret;
//	}
//	printf("%d\n", sum);  // 1444231215 不知道是否正确  我们可以把n改为n<=3试一下 看否正确
//	// 发现结果是15 不是9 出现错误了 实际的结果和预期不符 这就是bug
//	// 调试就能发现每算一个数的时候ret没有从1开始 
//	return 0;
//}

// 我们可以做个优化
//int main()
//{
//	int n = 0;
//	int i = 0;
//	int ret = 1;  // 不能从0开始 因为是乘
//	int sum = 0;
//	for (n = 1; n <= 3; n++)
//	{
//		ret *= n;   // 3的阶乘和四的阶乘之间只差*4所以我们可以这样来
//		sum += ret;
//	}
//	printf("%d\n", sum);  
//	return 0;
//}

//在VS2022、X86、Debug 的环境下，编译器不做任何优化的话，下⾯代码执⾏的结果是啥？
#include <stdio.h>
//int main()
//{
//	int i = 0;
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };// 0-9
//
//	// 调试的时候 发现越界访问数组 照样把arr[10]arr[11]arr[12]的改成0 
//	// 改变arr[12]为0的时候然后i又变成了0 一直这样循环
//
//	// 取地址观察发现 i与arr[12]的地址一样
//	for (i = 0; i <= 12; i++)
//	{
//		arr[i] = 0;
//		printf("hehe\n");
//	}
//	return 0;
//}

//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };// 0-9
//	int i = 0;  // 这样就不会死循环了，会报错，越界访问了
//	for (i = 0; i <= 12; i++)
//	{
//		arr[i] = 0;
//		printf("hehe\n");
//	}
//	return 0;
//}

// i 和 arr是局部变量  局部变量放在内存的栈区
//程序运⾏，死循环了，调试看看为什么？调试可以上⾯程序的内存布局如下：
//1. 栈区内存的使⽤习惯是从⾼地址向低地址使⽤的，所以变量i的地址是
//较⼤的。arr数组的地址整体是⼩于i的地址。
//2. 数组在内存中的存放是：随着下标的增⻓，地址是由低到⾼变化的。
//所以根据代码，就能理解为什么是左边的代码布局了。
//如果是左边的内存布局，那随着数组下标的增⻓，往后越界就有可能覆盖
//到i，这样就可能造成死循环的。这⾥肯定有同学有疑问：为什么i和arr
//数组之间恰好空出来2个整型的空间呢？这⾥确实是巧合，在不同的编译
//器下可能中间的空出的空间⼤⼩是不⼀样的，代码中这些变量内存的分配
//和地址分配是编译器指定的，所以的不同的编译器之间就有差异了。所以
//这个题⽬是和环境相关的。
//release版本存在优化,debug不存在优化，有可能release弄的时候和debug不一样 ，测试是测release 这样程序员和测试人员有时就有分期
//从这个理解我们能够体会到调试的重要性，只有调试才能观察到程序内部执⾏的细节，就像医⽣给病
//⼈做B超，CT⼀样。
//注意：栈区的默认的使⽤习惯是先使⽤⾼地址，再使⽤低地址的空间，但是这个具体还是要编译器的
//实现，⽐如：
//在VS上切换到X64，这个使⽤的顺序就是相反的，在Release版本的程序中，这个使⽤的顺序也是相反
//的。

//void test1(int arr3[])
//{
//	// 监视要看函数中所传的数组元素 要先数组名 再加上要看的个数才能看到想看的几个元素
//	// 不然只能看到一个  arr3,10 这样就能看到10个元素  arr3 只能看到一个
//}
//
//void test2(int arr4[3][5])
//{
//	// 监视的时候只能看到第一行 arr4只能看到第一行
//	// arr4,3就可以看到三行
//}
//
//int main()
//{
//	int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
//	int arr2[3][5] = { 1,2,3,4,5 ,2,3,4,5,6 ,3,4,5,6,7 };
//
//	test1(arr1);
//	test2(arr2);
//	return 0;
//}

//编译型错误
//编译型错误⼀般都是语法错误，这类错误⼀般看错误信息就能找到⼀些蛛丝⻢迹的，双击错误信息也
//能初步的跳转到代码错误的地⽅或者附近。编译错误，随着语⾔的熟练掌握，会越来越少，也容易解
//决。

//链接型错误
//看错误提⽰信息，主要在代码中找到错误信息中的标识符，然后定位问题所在。⼀般是因为
//• 标识符名不存在
//• 拼写错误
//• 头⽂件没包含
//• 引⽤的库不存在

//运行错误,结果错误，调试的方式解决  前面两种不可以使用调试，因为运行起来才能调试

//递归是学习C语⾔函数绕不开的⼀个话题，那什么是递归呢？
//递归其实是⼀种解决问题的⽅法，在C语⾔中，递归就是函数⾃⼰调⽤⾃⼰。
//写⼀个史上最简单的C语⾔递归代码:
//int main()
//{
//	printf("hehe\n");
//	main();
//	return 0;
//}

// 每一次函数调用，都要为这次函数调用分配内存空间，是从内存的栈区上分配的
//上述就是⼀个简单的递归程序，只不过上⾯的递归只是为了演⽰递归的基本形式，不是为了解决问
//题，代码最终也会陷⼊死递归，导致栈溢出（Stack overflow）。

//把⼀个⼤型复杂问题层层转化为⼀个与原问题相似，但规模较⼩的⼦问题来求解；直到⼦问题不能再
//被拆分，递归就结束了。所以递归的思考⽅式就是把⼤事化⼩的过程。
//递归中的递就是递推的意思，归就是回归的意思，接下来慢慢来体会
//
//递归在书写的时候，有2个必要条件：
//• 递归存在限制条件，当满⾜这个限制条件的时候，递归便不再继续。
//• 每次递归调⽤之后越来越接近这个限制条件。
//在下⾯的例⼦中，我们逐步体会这2个限制条件

//举例1：求n的阶乘  (不考虑溢出)

// n！=n*（n-1）!

//当 n == 0 的时候，n的阶乘是1，其余n的阶乘都是可以通过公式计算。
//n的阶乘的递归公式如下：
//Fact(n)
//1, n == 0
//n * Fact(n - 1), n > 0

// 递归是少量的代码完成了大量复杂的运算
//int Fact(int n)
//{
//	if (n == 0)
//		return 1;
//	else
//		return n*Fact(n - 1);
//}
//int main()
//{
//	int n = 0;
//	scanf("%d", &n);
//	int ret = Fact(n);
//	printf("%d", ret);
//	return 0;
//}

//举例2：输⼊⼀个整数m，打印这个按照顺序打印整数的每⼀位。

//Print(1234) 打印1234的每一位
//Print(123) 4 打印123的每一位，再打印4
//Print(12) 3 4 打印12的每一位  再打印3 4
//Print(1) 2 3 4 打印1 然后打印 2 3 4
//void Print(int n)
//{
//	if (n > 9)
//		Print(n / 10);
//	printf("%d ", n % 10);
//}
//
//int main()
//{
//	int n = 0;
//	scanf("%d", &n);
//	Print(n);
//	return 0;
//}


//递归是⼀种很好的编程技巧，但是很多技巧⼀样，也是可能被误⽤的，就像举例1⼀样，看到推导的公
//式，很容易就被写成递归的形式：
//
//Fact函数是可以产⽣正确的结果，但是在递归函数调⽤的过程中涉及⼀些运⾏时的开销。
//在C语⾔中每⼀次函数调⽤，都要需要为本次函数调⽤在栈区申请⼀块内存空间来保存函数调⽤期间
//的各种局部变量的值，这块空间被称为运⾏时堆栈，或者函数栈帧。
//函数不返回，函数对应的栈帧空间就⼀直占⽤，所以如果函数调⽤中存在递归调⽤的话，每⼀次递归
//函数调⽤都会开辟属于⾃⼰的栈帧空间，直到函数递归不再继续，开始回归，才逐层释放栈帧空间。
//所以如果采⽤函数递归的⽅式完成代码，递归层次太深，就会浪费太多的栈帧空间，也可能引起栈溢
//出（stack overflow）的问题。
//注：
//所以如果不想使⽤递归就得想其他的办法，通常就是迭代的⽅式（通常就是循环的⽅式）。
// 但是不一定是循环 循环是一种迭代 迭代不一定是循环
//⽐如：计算n的阶乘，也是可以产⽣1~n的数字累计乘在⼀起的。

//int Fact(int n)
//{
//	int i = 0;
//	int ret = 1;
//	for(i = 1 ; i <= n ; i++)
//	{
//		ret *= i;  // ret = ret *i
//	}
//	return ret;
//}
//int main()
//{
//	int n = 0;
//	scanf("%d",&n);
//	int r = Fact(n);
//	printf("%d\n", r);
//	return 0;
//}
//上述代码是能够完成任务，并且效率是⽐递归的⽅式更好的。
//事实上，我们看到的许多问题是以递归的形式进⾏解释的，这只是因为它⽐⾮递归的形式更加清晰，
//但是这些问题的迭代实现往往⽐递归实现效率更⾼。
//当⼀个问题⾮常复杂，难以使⽤迭代的⽅式实现时，此时递归实现的简洁性便可以补偿它所带来的运
//⾏时开销。

//举例3：求第n个斐波那契数
//我们也能举出更加极端的例⼦，就像计算第n个斐波那契数，是不适合使⽤递归求解的，但是斐波那契
//数的问题通过是使⽤递归的形式描述的，如下：
//n <= 2  An = 1;   n > 2  An = A(n - 1) + A(n - 2);

//int Fib(int n)
//{
//	if (n <= 2)
//		return 1;
//	else
//		return Fib(n - 1) + Fib(n - 2);
//}
//int main()
//{
//	int n = 0;
//	scanf("%d", &n);
//	int ret = Fib(n);
//	printf("%d", ret);
//	return 0;
//}

//当我们n输⼊为50的时候，需要很⻓时间才能算出结果，这个计算所花费的时间，是我们很难接受的，
//这也说明递归的写法是⾮常低效的，那是为什么呢？

//其实递归程序会不断的展开，在展开的过程中，我们很容易就能发现，在递归的过程中会有重复计
//算，⽽且递归层次越深，冗余计算就会越多。我们可以作业测试：

//int count = 0;
//int Fib(int n)
//{
//	if (n == 3)
//		count++;
//	if (n <= 2)
//		return 1;
//	else
//		return Fib(n - 1) + Fib(n - 2);
//}
//int main()
//{
//	int n = 0;
//	scanf("%d", &n);
//	int ret = Fib(n);
//	printf("%d\n", ret);
//	printf("count = %d", count);
//	return 0;
//}


//这⾥我们看到了，在计算第40个斐波那契数的时候，使⽤递归⽅式，第3个斐波那契数就被重复计算了
//39088169次，这些计算是⾮常冗余的。所以斐波那契数的计算，使⽤递归是⾮常不明智的，我们就得
//想迭代的⽅式解决。

// c = a + b
// a = b
// b = c  一直循环往复
// 1 1 2 3 5 8 ........  前两个不用算  所以第三个数算1次  第四个数算2次


//int Fib(int n)
//{
//	int a = 1;
//	int b = 1;
//
//	//int c = 0;  // n=1的时候不进入循环 所以c不能是0 返回0
//	int c = 1;
//	while (n>2)
//	{
//		c = a + b;
//		a = b;
//		b = c;
//		n--;
//	}
//
//	return c;
//}
//// 结果可能溢出 因为数字太大了
//int main()
//{
//	int n = 0;
//	scanf("%d", &n);
//	int r = Fib(n);
//	printf("%d\n", r);
//	return 0;
//}

//递归在书写的时候，有2个必要条件：
//• 递归存在限制条件，当满⾜这个限制条件的时候，递归便不再继续。
//• 每次递归调⽤之后越来越接近这个限制条件。
// 满足这个条件的递归可能也太不行 也可能死递归
//void test(int n)
//{
//	printf("%d ", n);
//	if (n <= 10000)
//	{
//		test(n + 1);
//	}
//}
//int main()
//{
//	test(1);
//	return 0;
//}

//其实我们经常能听到2进制、8进制、10进制、16进制这样的讲法，那是什么意思呢？其实2进制、8进
//制、10进制、16进制是数值的不同表⽰形式⽽已。
//⽐如：数值15的各种进制的表⽰形式：
//15的2进制：1111
//15的8进制：17
//15的10进制：15
//15的16进制：F

//⾸先我们还是得从10进制讲起，其实10进制是我们⽣活中经常使⽤的，我们已经形成了很多尝试：
//• 10进制中满10进1
//• 10进制的数字每⼀位都是0~9的数字组成
//其实⼆进制也是⼀样的
//• 2进制中满2进1
//• 2进制的数字每⼀位都是0~1的数字组成
//那么 1101 就是⼆进制的数字了。

//其实10进制的123表⽰的值是⼀百⼆⼗三，为什么是这个值呢？其实10进制的每⼀位是权重的，10进
//制的数字从右向左是个位、⼗位、百位....，分别每⼀位的权重是 10的0次方, 10的1次方, 10的2次方 ...

//2进制和10进制是类似的，只不过2进制的每⼀位的权重，从右向左是: 2的0次方, 2的1次方, 2的2次方 ...

//2进制 0 - 1组成  8进制 0 - 7 组成  10进制 0 - 9组成  16进制 0 - 9 a b c d e f组成

// int的长度是4个字节 == 32bit位
// 当我们要把一个数转换成2进制形式表示的时候 整数的二进制就有三种表示形式 暂时不考虑浮点数.
//整数的2进制表⽰⽅法有三种，即原码、反码和补码
//有符号整数的三种表⽰⽅法均有符号位和数值位两部分，2进制序列中，最⾼位的1位是被当做符号
// 无符号整数 >=0 所有的位都是数值位
//位，剩余的都是数值位。
//符号位都是⽤0表⽰“正”，⽤1表⽰“负”。

//正整数的原、反、补码都相同。
//负整数的三种表⽰⽅法各不相同。
//原码：直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
//反码：将原码的符号位不变，其他位依次按位取反就可以得到反码。
//补码：反码 + 1就得到补码。
//补码得到原码也是可以使⽤：取反， + 1的操作。
// 原码 （取反）-> 反码 （+1）-> 补码     补码（-1）-> 反码 (取反) -> 原码
//int main()
//
//{
//	int b = 10;
//	//00000000000000000000000000001010 -- 原码
//	//00000000000000000000000000001010 -- 反码
//	//00000000000000000000000000001010 -- 补码
//	int a = -10; 
//	//10000000000000000000000000001010 -- 原码
//	//11111111111111111111111111110101 -- 反码
//	//11111111111111111111111111110110 -- 补码 // 加1之后进一位
//
//	//11111111111111111111111111110110 -- 补码
//	//10000000000000000000000000001001
//	//10000000000000000000000000001010 -- 原码
//
//		
//	return 0;
//}

//对于整型来说：数据存放内存中其实存放的是补码。
//为什么呢？
//在计算机系统中，数值⼀律⽤补码来表⽰和存储。原因在于，使⽤补码，可以将符号位和数值域统⼀
//处理；同时，加法和减法也可以统⼀处理（CPU只有加法器）此外，补码与原码相互转换，其运算
//过程是相同的，不需要额外的硬件电路。

//int main()
//{
//	// 1-1
//	// 1+(-1)
//	// 尝试用原码计算
//	// 00000000000000000000000000000001 --  1的原码
//	// 10000000000000000000000000000001 --  -1的原码
//	// 10000000000000000000000000000010 --  -2  如果直接相加  结果是错误的
//
//	// 尝试使用补码计算
//	// 00000000000000000000000000000001 --   1的补码
//	// 10000000000000000000000000000001 --  -1的原码
//	// 11111111111111111111111111111110 --  -1的反码
//	// 11111111111111111111111111111111 --  -1的补码   -1的补码是全1
//	// 00000000000000000000000000000001 --   1的补码
//	// 100000000000000000000000000000000 --  -1补码+1的补码    2进制逢2进1 不断一直进1  只能存32bit位 多了一位不要 这位就是0 计算正确
//	// 所以数据存放内存中其实存放的是补码。
//}